    org 0x7c00
BaseOfStack     equ 0x7c00                  ;存放栈顶
BaseOfLoader    equ 0x1000                  ;存放 Loader 启动程序的首地址
OffsetOfLoader  equ 0x00                    ;存放 Loader 启动程序的偏移值

RootDirSectors              equ 14          ;根目录占用的扇区数
SectorNumOfRootDirStart     equ 19          ;根目录的起始扇区号
SectorNumOfFAT1Start        equ 1           ;FAT1表的起始扇区号
SectorBalance               equ 17          ;

    jmp short Label_Start                   ;一个短跳转指令
    nop                                     ;凑够3个字节
    BS_OEMName          db  'MINEboot'      ;厂商名
    BPB_BytesPerSec     dw  512             ;每扇区字节数（Bytes/Sector）
    BPB_SecPerClus      db  1               ;每簇扇区数（Sector/Cluster）
    BPB_ResvdSecCnt     dw  1               ;Boot记录占用多少扇区
    BPB_NumFATs         db  2               ;共有多少FAT表
    BPB_RootEntCnt      dw  224             ;根目录区文件最大数
    BPB_TotSec16        dw  2880            ;扇区总数
    BPB_Media           db  0xf0            ;介质描述符
    BPB_FATSz16         dw  9               ;每个FAT表所占扇区数
    BPB_SecPerTrk       dw  18              ;每磁道扇区数（Sector/track）
    BPB_NumHeads        dw  2               ;磁头数（面数）
    BPB_HiddSec         dd  0               ;隐藏扇区数
    BPB_TotSec32        dd  0               ;如果 BPB_TotSec16=0,则由这里给出扇区数
    BS_DrvNum           db  0               ;INT 13H 的驱动器号
    BS_Reserved1        db  0               ;保留，未使用
    BS_BootSig          db  0x29             ;扩展引导标记(29h)
    BS_VolID            dd  0               ;卷序列号
    BS_VolLab           db  'boot loader'   ;卷标
    BS_FileSysType      db  'FAT12'         ;文件系统类型


Label_Start:
;1. 下面四行代码是将DS、ES、SS寄存器初始化
    mov     ax,     cs
    mov     ds,     ax
    mov     es,     ax
    mov     ss,     ax
;1> 下面这行代码是指定栈顶为0x7c00，这个是固定的，是历史遗留
    mov     sp,     BaseOfStack

;2. 下面这五行代码是把dos屏幕清屏操作，此时BX、CX、DX寄存器不起作用
;int 10h和ah=06，且al=0的时候就是清屏,如果al不为0的时候，就是翻卷屏幕
    mov     ax,     0600h
    mov     bx,     0700h
    mov     cx,     0
    mov     dx,     0184fh
    int     10h

;3. 下面的这四行代码是设置光标的代码
;int 10h和ah=02的时候，就是设置光标，此时BH代表着页码，
;DH=游标的列数，DL=游标的行数
    mov     ax,     0200h
    mov     bx,     0000h
    mov     dx,     0000h
    int     10h

;4. 下面的代码是在屏幕上显示字符串
    mov     dh,     byte[CurrentShowLine]
    mov     dl,     00h
    mov     cx,     10
    mov     bp,     StartBootMessage
    call    Func_ShowStrGreen
    add     byte[CurrentShowLine], 1

;5. 下面的代码是将软盘驱动器复位，相当于初始化了一遍软盘驱动器
    xor     ah,     ah
    xor     dl,     dl
    int     13h

;6. 下面的代码是查找 loader 文件的
    mov     word[SectorNo], SectorNumOfRootDirStart ;将根目录的起始扇区号，赋值给临时变量 SectorNo
    
Label_Search_In_Root_Dir_Begin:
    cmp     word[RootDirSizeForLoop], 0 ;根目录占用的扇区数与0比较，如果等于0，就报错 
    jz      Label_No_LoaderBin
    dec     word[RootDirSizeForLoop]    ;将根目录占用的扇区数减1
    mov     ax,     00h
    mov     es,     ax          ;将ES改成00h
    mov     bx,     8000h       ;将BX改为8000H，ES:BX=接受软盘数据的数据缓冲区起始地址
    mov     ax,     [SectorNo]  ;通过AX传参
    mov     cl,     1           ;通过CL传参
    call    Func_ReadOneSector
    mov     si,     LoaderFileName  ;将提示语句地址传入SI
    mov     di,     8000h
    cld                     ;告诉CPU现在可以从前往后对比了
    mov     dx,     10h     ;每个扇区可容纳的目录项个数(512/32=16=0x10)
    
Label_Search_For_LoaderBin:
    cmp     dx,     0
    jz      Label_Goto_Next_Sector_In_Root_Dir  ;如果这个扇区没有找到，就下个扇区
    dec     dx
    mov     cx,     11      ;目录项的文件名长度为11B，这里是十进制的

Lable_Cmp_FileName:
    cmp     cx,     0
    jz      Label_FileName_Found    ;如果找到，就提示，并进入到下个步骤
    dec     cx
    lodsb                       ;将 SI 指向的值放到 AL 里面，只有一个字节
    
    cmp     al,     byte[es:di] ;比较字节是否一致
    jz      Label_Go_On         ;如果一致，就继续比较下一个字节
    jmp     Label_Different     ;如果不一样，就换下个目录项

;当第一个字符一致的时候，就继续
Label_Go_On:
    inc     di                  ;更换为下个字节
    jmp     Lable_Cmp_FileName  ;跳回去，继续比较

;当第一个字符不一致的时候，就会换下个目录项
Label_Different:
    and     di,     0ffe0h      ;0ffe0h=0b1111111111100000，将DI的低5位清零。因为每个条目的大小是32字节，32种变化对应的位数是5位，这时就正好指向本条目的开头
    add     di,     20h         ;这里是因为一个目录项大小为32B，十六进制就是20H
    mov     si,     LoaderFileName  ;将文件的地址重新传给 SI 从头开始比较
    jmp     Label_Search_For_LoaderBin  ;继续回去比较

Label_Goto_Next_Sector_In_Root_Dir:
    add     word[SectorNo], 1   ;换下一个扇区
    jmp     Label_Search_In_Root_Dir_Begin

;这段代码是显示没有找到 Loader 程序文件的错误信息，然后就让CPU停在这里
Label_No_LoaderBin:
;这里没有保存现场是因为不需要，我们就是不让运行了，让用户重新开机
    mov     dh,     byte[CurrentShowLine]
    mov     dl,     00h
    mov     cx,     21
    mov     bp,     NoLoaderMessage
    call    Func_ShowStrGreen
    add     byte[CurrentShowLine], 1
    jmp     $

;7. 找到 Loader 程序后，把它加载到内存中，位置已经被指定了
Label_FileName_Found:
    mov     ax,     RootDirSectors  ;RootDirSectors -> 根目录占用的扇区数
    and     di,     0ffe0h          ;这里跟上面的原因一样
    add     di,     01ah            ;1ah=26，就是将 DI 指向起始簇号，这里由目录项结构来的，不知道的话，可以去百度一下目录项结构
    mov     cx,     word[es:di]     ;将起始簇号暂时保存到 CX 里面
    push    cx                      ;将起始簇号暂时保存到栈里面
    add     cx,     ax              ;根目录占用的扇区数 + 起始簇号 = 文件的磁盘起始扇区号版本1
    add     cx,     SectorBalance   ;文件的磁盘起始扇区号版本1 + 扇区保留 = 文件的磁盘起始扇区号
    mov     ax,     BaseOfLoader    ;将 Loader 程序的基址放到 ax 中 
    mov     es,     ax              ;再将 Loader 程序的基址最终赋值给 ES
    mov     bx,     OffsetOfLoader  ;再将 Loader 程序的偏移赋值给 BX 这两步是准备读取软盘内容
    mov     ax,     cx              ;准备使用软盘读取函数了

;下面这段代码是用来显示一个 . 用的是 int 10h 和 AH = 0EH 的中断程序
Label_Go_On_Loading_File:
    push    ax
    push    bx
    mov     ah,     0eh
    mov     al,     '.'
    mov     bl,     0fh
    int     10h
    pop     bx
    pop     ax
    
    mov     cl,     1               ;这里是只读取一个扇区
    call    Func_ReadOneSector
    pop     ax                      ;AX当前值是文件的起始簇号
    call    Func_GetFATEntry
    cmp     ax,     0fffh           ;这里是判断是否是最后一个簇，FAT表项中 FFFH代表着是最后一个簇
    jz      Label_File_Loaded
    push    ax
    mov     dx,     RootDirSectors  ;这里重置 AX 为根目录占用的扇区数
    add     ax,     dx
    add     ax,     SectorBalance   ;重新计算机出文件的磁盘起始扇区号
    add     bx,     [BPB_BytesPerSec]   ;指向下一段未使用的内存，说白了就是指向下一个512扇区
    jmp     Label_Go_On_Loading_File

;8. 跳到 loader
Label_File_Loaded:
    jmp     BaseOfLoader:OffsetOfLoader ;这个是段间跳转，属于长跳转，BaseOfLoader 是引导程序的基址，OffsetOfLoader 是引导程序的偏移


;软盘读取函数
;参数为：AX传入待读取的磁盘起始扇区号
;        CL传入的是读入的扇区数量
;        ES:BX设置为目标缓冲区起始地址
Func_ReadOneSector:
    push    bp
    push    dx
    mov     bp,     sp
    sub     esp,    2  ;为函数开辟了2字节的临时内存空间
    mov     byte[bp - 2], cl    ;将读入的扇区数量放到临时内存里面
    push    bx
    mov     bl,     [BPB_SecPerTrk]
    div     bl  ;待读取的磁盘起始扇区号 / 每磁道扇区数（Sector/track）
    inc     ah  ;求的余数自增1, 起始扇区号=余数 + 1，起始扇区号是从1开始的
    mov     cl,     ah  ;将求到的起始扇区号放到 CL 里面
    mov     dh,     al  ;将求的商（前7位是磁道号，后1位是磁头号）放到 DH 里面
    shr     al,     1   ;将商（磁头号）右移1位，得到磁道号
    mov     ch,     al  ;将磁道号放到 CH 里面
    and     dh,     1   ;将 DH 除了最后一位全部置为 0,每个磁片有两个磁头，磁头数只有0和1
    pop     bx
    mov     dl,     [BS_DrvNum] ;驱动器号

Label_Go_On_Reading:
    mov     ah,     2
    mov     al,     byte[bp - 2]    ;读入的扇区数
    int     13h
    jc Label_Go_On_Reading  ;如果数据读取成功的话，会把CF标志位复位，这里不会跳转
    add     esp,    2
    pop     dx
    pop     bp
    ret

;函数：打印绿色的字符串
;参数为：dx 在哪一行，哪一列打印
;        cx 要打印的字符串的长度
;        bp 要打印的字符串的偏移
Func_ShowStrGreen:
    push    bp
    push    ax
    push    bx
    mov     ax,     1301h
    mov     bx,     0002h
    push    ax
    mov     ax,     ds
    mov     es,     ax
    pop     ax
    int     10h
    pop     bx
    pop     ax
    pop     bp
    ret

;函数：找到序号为 AX 的扇区在 FAT 中的条目
;参数为：AX 为 FAT表项号
;返回值：存放在 AX 里面，是文件的簇号
Func_GetFATEntry:
    push    es
    push    bx
    push    dx
    push    ax
    mov     ax,     00
    mov     es,     ax      ;这里是要读取软盘了，所以需要给ES一个值
    pop     ax
    mov     byte[Odd],  0   ;将奇偶标志复位
    mov     bx,     3
    mul     bx              ;AX = AX * 3 
    mov     bx,     2
    div     bx              ;AX = AX / 2，余数放到 DX 里面
    cmp     dx,     0       ;如果余数为0，说明是偶数
    jz      Label_Even
    mov     byte[Odd],  1   ;如果余数不为0，说明是奇数，此时将 Odd 置为 1

Label_Even:
    xor     dx,     dx      ;将 DX 清零
    mov     bx,     [BPB_BytesPerSec]   ;BPB_BytesPerSec是每个扇区的字节数
    div     bx              ;AX = AX / BPB_BytesPerSec，此时 AX 就是FAT表项的偏移扇区号
    mov     bx,     8000h   ;BX原来的值，函数开始的时候就保存了，这里是为了读取软盘，才设置的
    add     ax,     SectorNumOfFAT1Start    ;SectorNumOfFAT1Start是FAT起始扇区号，此时加上它是为了保证 AX 是非0的
    mov     cl,     2       ;要读的扇区数量
    call    Func_ReadOneSector
    add     bx,     dx      ;BX + DX = 表项在内存中的地址，DX 放的是余数，余数是FAT表项在扇区中的偏移位置
    mov     ax,     [es:bx] ;将表项内容读取到 AX 里面
    cmp     byte[Odd],  1   ;判断是否奇偶
    jnz     Label_Even_2    
    shr     ax,     4       ;右移4位是因为只有12位是有效的，而奇数是前12位有效

Label_Even_2:
    and     ax,     0fffh   ;将最高的4位清零，因为他们没有用，FAT表项占用12bit
    pop     dx
    pop     bx
    pop     es
    ret

;临时变量区域
RootDirSizeForLoop  dw  RootDirSectors  ;用来循环查询扇区的时候控制扇区数量的
SectorNo            dw  0               ;扇区号
Odd                 db  0               ;奇偶标志
CurrentShowLine     db  0               ;当前要打印到的行数

;提示字符串区域，注意字符串最好要少写，因为引导磁盘只有 512B 字符串太长的话就放不下了
StartBootMessage: db    "Start Boot"
NoLoaderMessage:  db    "ERROR:No Loader Found"
LoaderFileName:   db    "LOADER  BIN", 0

times 510 - ($ - $$) db 0
dw 0xaa55
